package ch.fhnw.ams.sudoku;

import java.util.Date;

import org.apache.log4j.Logger;

import ch.fhnw.ams.sudoku.io.SudokuDAO;
import ch.fhnw.ams.sudoku.io.SudokuDAOException;
import ch.fhnw.ams.sudoku.io.SudokuFileDAO;

/**
 * Represents the game logic. Only one instance exists at one time. The Game has
 * three possible States: Playing, Creating and Solving ({@link SudokuState}).
 * If the state of the game changes, all registered observers are notified.
 * 
 * @author amsler
 * 
 */
public class SudokuGame extends SudokuObservable {
	
	private final static Logger log = Logger.getLogger(SudokuGame.class);
	
	/** the initial state of the game */
	public final static SudokuState DEFAULT_STATE = SudokuState.CREATING;
	
	/**
	 * The states of the {@link SudokuGame}
	 */
	public enum SudokuState {
		PLAYING,
		CREATING,
		SOLVING,
	}
	
	private SudokuDAO sudokuDAO;
	private SudokuGrid sudokuGrid;
	private final ISolver solver;
	private String gameId;
	
	private SudokuState state = DEFAULT_STATE;
	
	
	
	/**
	 * Constructs a new Sudoku game
	 */
	public SudokuGame() {
		this(new Recursive9by9Solver());
	}
	
	
	/**
	 * Constructs a new Sudoku game with a custom solver
	 */
	public SudokuGame(ISolver solver) {
		this.solver = solver;
		sudokuDAO = new SudokuFileDAO();
		sudokuGrid = new SudokuGrid();
		gameId = "";
		newGame();
	}
	
	/**
	 * Solves a Sudoku puzzle automatically. The algorithm is described in {@link ISolver}.
	 * 
	 * @return <code>true</code> if a Solution for the puzzle exists
	 */
	public synchronized boolean solve() {
		boolean ret = true;
		changeState(SudokuState.SOLVING);
		
		int[][] data = sudokuGrid.asArray(false);
		log.debug("solving:\n" + SudokuUtil.toSudokuString(data));
		
		try {
			solver.setData(data);
			long time_ms = System.currentTimeMillis();
			solver.generateSolution();
			time_ms = System.currentTimeMillis() - time_ms;
			if (solver.hasSolution()) {
				int[][] solution = solver.getSolution();
				sudokuGrid.setSolution(solution);
				log.debug("solved! required time: " + time_ms + " ms" + "\n"
						+ SudokuUtil.toSudokuString(sudokuGrid.asArray(false)));
			} else {
				ret = false;
				log.debug("no solution found");
			}
		} catch (InvalidSudokuException shouldNotHappen) {
			log.debug(shouldNotHappen);
			ret = false;
		}
		
		changeState(SudokuState.PLAYING);
		return ret;
	}
	
	/**
	 * Loads an existing Sudoku.
	 * 
	 * @param id
	 *            the path to the Sudoku file
	 * @return <code>false</code> if the file could not be read
	 */
	public synchronized boolean load(String id) {
		try {
			sudokuDAO.load(id);
			sudokuGrid = sudokuDAO.getSudokuGrid();
		} catch (SudokuDAOException e) {
			log.error(e.getMessage(), e);
			return false;
		}
		
		gameId = id;
		changeState(SudokuState.PLAYING);
		
		log.debug("load game");
		log.debug(this);
		return true;
	}
	
	/**
	 * Saves an existing Sudoku.
	 * 
	 * @param id
	 *            the path to the Sudoku file
	 * @return <code>false</code> if the file could not be saved
	 */
	public synchronized boolean save(String id) {
		try {
			sudokuDAO.setSudokuGrid(sudokuGrid);
			sudokuDAO.save(id);
		} catch (SudokuDAOException e) {
			log.error(e.getMessage(), e);
			return false;
		}
		
		gameId = id;
		log.debug("save game");
		log.debug(this);
		return true;
	}
	
	/**
	 * Resets the game to the initial state.
	 */
	public synchronized void newGame() {
		sudokuGrid.clear();
		setCreationDate(new Date());
		setTimePlayed(0);
		gameId = "";
		changeState(SudokuState.CREATING);
		
		log.debug("new game");
		log.debug(this);
	}
	
	/**
	 * Changes the State of the game from {@link SudokuState#CREATING} to
	 * {@link SudokuState#PLAYING}
	 */
	public synchronized void play() {
		for (SudokuField field : sudokuGrid) {
			if (field.isEmpty()) {
				field.setPuzzelNumber(false);
			}
		}
		changeState(SudokuState.PLAYING);
	}
	
	/**
	 * Changes the State of the game from {@link SudokuState#PLAYING} to
	 * {@link SudokuState#CREATING}
	 */
	public synchronized void edit() {
		sudokuGrid.reset();
		for (SudokuField field : sudokuGrid) {
			field.setPuzzelNumber(true);
		}
		changeState(SudokuState.CREATING);
	}
	
	private void changeState(final SudokuState newState){
		SudokuState oldState = state;
		state = newState;
		notifyObservers(oldState, newState);
	}
	
	@Override
	public String toString() {
		StringBuffer buf = new StringBuffer();
		buf.append("SudokuGame\n");
		buf.append("State=" + state + "\n");
		buf.append("Grid:\n");
		buf.append(sudokuGrid.toString());
		return buf.toString();
	}
	
	public void setPlayerName(String playerName) {
		sudokuDAO.setPlayerName(playerName);
	}
	
	public String getPlayerName() {
		return sudokuDAO.getPlayerName();
	}
	
	public void setTimePlayed(long timePlayed) {
		this.sudokuDAO.setTimePlayed(timePlayed);
	}

	public long getTimePlayed() {
		return sudokuDAO.getTimePlayed();
	}

	public SudokuGrid getSudokuGrid() {
		return sudokuGrid;
	}

	public SudokuState getState() {
		return state;
	}
	
	public void setCreationDate(Date creationDate) {
		this.sudokuDAO.setCreationDate(creationDate);
	}

	public Date getCreationDate() {
		return sudokuDAO.getCreationDate();
	}

	public String getGameId() {
		return gameId;
	}
}
